#!/opt/ActivePerl-5.10/bin/perl
#/usr/bin/perl

use strict;
use warnings;
use constant true  => 1;
use constant false => 0;

use File::Copy;
use File::Path;
use File::Compare;
use Config::IniFiles;
use Getopt::Std;
use Cwd;
use Data::Dumper;
use Benchmark::Stopwatch::Pause;


###############################
# Missing features
###############################
# Fast compraison algorithm : md5
# FTP backup
# SSH backup
# HTTP backup with php script
# daemon version scheduling backup
# email alert
# jabber alert
# user interface
# restore interface
# multiple destination
# Encrypted version with key
# passive trigger by udp request
# Dependencies handler

my $Version="0.91";

my $bench=getStopwatch();
$bench->start if $bench;
$bench->lap("configuration");

my $sep="\\";
$sep="/" if $^O eq "linux";

my %opts;
#Config file rreeading
getopts( "c:p:", \%opts );
my $configFile=$opts{c};
my $profile=$opts{p};
$configFile=&Cwd::cwd().$sep."backup.ini" unless defined $configFile;
print STDERR "'$configFile' is missing. please setup a configuration file." unless -f "$configFile"; 

my $cfg = new Config::IniFiles( -file => "$configFile" );
print STDERR "problem while reading $configFile. please setup a configuration file." unless defined $cfg; 
$profile='Default' unless defined $profile;

my $logFile = $cfg->val($profile, 'logFile');
$logFile='-' unless defined $logFile;

my $outputLog = checkLogFile($logFile) or die("Error opening log file -- $@");
logIt("#"x80);
logIt("Starting a backup from ConfigFile : $configFile, Profile:$profile");
logIt("#"x80);

my $backupOldFiles=1;
$backupOldFiles=eval {$cfg->val($profile, 'backupOldFiles')} if defined $cfg->val($profile, 'backupOldFiles') ; 

my $trashBin = $cfg->val($profile, 'trashDirectory');
my $ldir= $cfg->val($profile, 'sourceDirectory');
my $rdir= $cfg->val($profile, 'destinationDirectory');

fatal_error("Missing source directory in profile : $profile") unless defined $ldir;
fatal_error("Missing destination directory in profile : $profile") unless defined $rdir;
fatal_error("Missing trash directory in profile : $profile") unless defined $trashBin;

my @exclusion_list =();
push @exclusion_list, $cfg->val($profile, 'excludePattern') if defined  $cfg->val($profile, 'excludePattern') ;
push ( @exclusion_list, '^\.$'   );
push ( @exclusion_list, '^\.\.$' );
logIt ("Tool version : $Version");
logIt ("Exclude list :  ". join (", ", @exclusion_list) );
logIt ("Source directory :  ". $ldir );
logIt ("Destination directory :  ". $rdir );
logIt ("Trashing files : ". $backupOldFiles );
logIt ("Trash directory :  ". $trashBin );
logIt("#"x80);

chdir $ldir or fatal_error("Couldn't change to $ldir");

unless (-d "$rdir") {
    eval { mkpath($rdir) };
    if ($@) {
	fatal_error ("Couldn't create $rdir: $@");
    }
}

fatal_error("Couldn't open the remote directory $rdir") unless -d "$rdir";

my $remoteFileName;
my $tmpDirName;
my $tmpRemDirName;
#$bench->lap("copy");
read_copy($ldir, $rdir);
aggregateSummary("RESULT:", $bench);
######################
# SUBROUTINES        #
######################

sub read_copy {
    my $local_dir = $_[0] or fatal_error("No local directory specified. $!");
    my $remote_dir = $_[1] or fatal_error("No remote directory specified. $!");
    my ($ft, $f);
    $bench->lap('create dir');
    if (! -d $remote_dir ) {
	logIt ("Creating directory $remote_dir");
	mkdir($remote_dir) or fatal_error("Failed to create $remote_dir. $!");
    }

    if ( $backupOldFiles == 1 ) {
	$bench->lap('trash file');
## BEGIN TRASH ROUTINE ##
	opendir($ft, $remote_dir) || fatal_error("Cannot opendir $remote_dir. $!");
	foreach my $name (sort readdir($ft)) {
	    if (! check_exclusion($name) ) {
		my $remoteFileName = $remote_dir . $sep . $name;
		my $localFileName = $local_dir . $sep . $name;
		if(! -e $localFileName) {
		    if(! -d $trashBin ) {
			fatal_error("Trash directory ($trashBin) doesn't seem to exist? $!");
		    }
		    else {
			my $trashFileName = getTrashFileName($name);
			logIt ("trashing $remoteFileName");
			move("$remoteFileName","$trashFileName") or die qq(Cannot move "$remoteFileName" to "$trashFileName": $!);
		    }
		}
	    } else {
		logIt("Exclude : $remote_dir$sep$name") unless $name eq '.' or $name eq '..';
	    }
	}
	closedir($ft);
## END TRASH ROUTINE ##
    }
    $bench->lap('copy file');
    opendir($f, $local_dir) || fatal_error("Cannot opendir $local_dir. $!");
    foreach my $name (sort readdir($f)) {
	if (! -d $name ) {
	    my $remoteFileName = $remote_dir . $sep . $name ;
	    my $localFileName = $local_dir . $sep . $name;

	    if (! -d $remote_dir) {
		fatal_error("$remoteFileName doesn't seem to exist? $!");
	    }

	    if (! -d $localFileName) {
		if ( checkBackup($localFileName, $remoteFileName) ) {
		    copy("$localFileName", "$remoteFileName") or die qq(Cannot copy "$localFileName" to "$remoteFileName": $!) ;
		    logIt("Mirroring $localFileName => $remoteFileName");
		}
	    }
	    elsif ( -d $localFileName && (! check_exclusion($name)) ) {
		my $tmpDirName = $local_dir . $sep . $name;
		my $tmpRemDirName = $remote_dir . $sep . $name;
		read_copy($tmpDirName, $tmpRemDirName);
	    }
	}
	else {
	    if (! check_exclusion($name) ) {
		my $dir1 = $local_dir . $sep . $name;
		my $dir2 = $remote_dir . $sep . $name;
		read_copy($dir1, $dir2);
	    }
	}

    }
    closedir($f);
}

sub checkBackup {
    my $file1 = $_[0] or fatal_error("Missing first file in checkBackup(). $!");
    my $file2 = $_[1] or fatal_error("Missing second file in checkBackup(). $!");
#check to see if we should back these files up
    if (compare("$file1","$file2") == 0) {
	return 0;
    }
    else {
	return 1;
    }
}


sub check_exclusion {
#checks the exclusion list for stuff not to copy
    my $file = $_[0];
    foreach my $item (@exclusion_list) {
	next unless defined $item;
	if ($file =~ /$item/) {
	    return 1;
	}
    }
    return 0;
}

sub fatal_error {
#die and log the error
    my $error_message = $_[0];
    $error_message = "Fatal Error: " . $error_message;
    logIt($error_message);
    die ($error_message . "\n");
}

# Finds a unique/valid file name for the file/directory to trash
sub getTrashFileName {
    my $file = $_[0];
    my $baseTrashName = $trashBin . $sep . $file;
    my $testTrashName = $baseTrashName;
    my $n = 0;
    while( -e $testTrashName ) {
	$n ++;
#if( $n > 99 ) {
#    fatal_error("too many files in the trash with base name: $baseTrashName" );
#}
	$testTrashName = $baseTrashName . '.' . $n;
    }
    return $testTrashName;
}

#####################
# Logging #
#####################

sub logIt {
#logs errors.
    my $message = $_[0];
    my @fullTime = localtime(time());
    my $time = ($fullTime[5] + 1900) . "/" . (addZero($fullTime[4] + 1)) . "/";
    $time .= addZero($fullTime[3]). " " . addZero($fullTime[2]) . ":";
    $time .= addZero($fullTime[1]) . ":" . addZero($fullTime[0]);
    my $logMessage = $time . "> $message\n";
    print $outputLog $logMessage;
}

sub checkLogFile {
#checks the output files to make sure they're valid
    my $file = $_[0];

    return *STDERR if ($file eq '-');
    my $openFile;
    my $status = (stat($file))[7];
    if (! $status) { $status = 0;}
    if ( $status != 0) {
	open($openFile, ">>" . $file) or die("Couldn't open log file for appending " . $file);
	return $openFile;
    }
    else {
	open($openFile, ">" . $file) or die("Couldn't create new log file " . $file);
	return $openFile;
    }
}

sub addZero {
#formats output to double digit
    if (length($_[0]) < 2) {
	return "0" . $_[0];
    }
    else {
	return $_[0];
    }
} 
sub getStopwatch {
    return Benchmark::Stopwatch::Pause->new();
}

sub aggregateSummary {
    my $label=shift;
    my $sw=shift;
    $sw->stop;

    my $data=$sw->as_data;

    my %aggr= map { $_->{'name'} =>  { 'elapsed' => 0, 'iter'=> 0, 'min'=>100000000000000, 'max'=>0 } } @{$$data{'laps'}};
    map { 
	$aggr{ $_->{'name'} }{'elapsed'} +=  $_->{'elapsed_time'};
	$aggr{ $_->{'name'} }{'iter'} ++;

	$aggr{ $_->{'name'} }{'min'}= $_->{'elapsed_time'} if  $_->{'elapsed_time'}<$aggr{ $_->{'name'} }{'min'};
	$aggr{ $_->{'name'} }{'max'}= $_->{'elapsed_time'} if  $_->{'elapsed_time'}>$aggr{ $_->{'name'} }{'max'};

    } @{$$data{'laps'}};

    map { 
	$aggr{$_}{'avg_iter'} =  ($aggr{$_}{'elapsed'})/($aggr{$_}{'iter'});
	$aggr{$_}{'pct_total'} =  100*($aggr{$_}{'elapsed'})/($sw->total_time);

    } keys %aggr;

    logIt("#"x80);
    logIt ($label);
    logIt("#"x80);
    logIt ("NAME\t\t\tNB ITER\tMIN ITER\tMAX ITER\tAVG ITER\tTOTAL\t\tAVG TOTAL");
    foreach my $lab ( keys %aggr ) {
	my $line = sprintf("%15.15s\t\t%d\t%.5f\t\t%.5f\t\t%.5f\t\t%.5f\t\t%.5f", 
		$lab,
		$aggr{$lab}{'iter'},
		$aggr{$lab}{'min'},
		$aggr{$lab}{'max'},
		$aggr{$lab}{'avg_iter'},
		$aggr{$lab}{'elapsed'},
		$aggr{$lab}{'pct_total'}
		);

	logIt ($line);
    }
    logIt("#"x80);
    logIt("Total Time: ".$$data{'total_elapsed_time'});
    logIt("#"x80);
}
